在昨天講完基本的 Redis 操作後,今天就讓我們修改 Day20 所做的公開匿名聊天室,結合 Redis 製作 隨機一對一匿名聊天室
吧!
整體的概念簡單來說會分成幾個部分
根據連線進來的 session
我們會給予隨機的 uuid
,這個 id 用於後續的配對與收發訊息用。
一開始連線進來時會查詢 wait
這個 key
內 list
的第一筆資料,如果有值就進行配對,反之就是自己進入等待列表。
聊天雙方的 key 互相成為對方的 value,例如 key 為 A 與 B,則 A 的 value 變成 B,B 的 value 變成 A。
只要有一方離開聊天室時,雙方的配對的 Key 就會取消,這個時候要回到第二步重新等待。
首先我們要先透過 session 取得作為識別的 uuid
,因此要先透過 go get
的方式安裝 package。
go get github.com/google/uuid
接著我們寫一個方法,參數為 melody
的 session
,透過此方法我們可以將 session 中的 chat_id
設定成隨機產生的 uuid
。
const KEY = "chat_id"
func InitSession(s *melody.Session) string {
id := uuid.New().String()
s.Set(KEY, id)
return id
}
接著我們透過 Get 的方式取得相關的 session id,如果沒有的話就進行初始化。
func GetSessionID(s *melody.Session) string {
if id, isExist := s.Get(KEY); isExist {
return id.(string)
}
return InitSession(s)
}
接著我們開始實作排隊的機制,這邊將方法分開寫。
首先可以透過查詢 wait
list 的第一筆資料。
func GetWaitFirstKey() (string, error) {
return redisClient.LPop(context.Background(), WAIT).Result()
}
透過 LPush
的方法將 id 放到 list 的尾端。
func AddToWaitList(id string) error {
return redisClient.LPush(context.Background(), WAIT, id).Err()
}
配對的部分主要是要將配對的兩個 id 進行綁定,移除的部分主要是要將兩個 id 從 redis 中刪除。
將兩個 id 進行配對。
func CreateChat(id1, id2 string) {
redisClient.Set(context.Background(), id1, id2, 0)
redisClient.Set(context.Background(), id2, id1, 0)
}
刪除兩個 id 的 key
func RemoveChat(id1, id2 string) {
redisClient.Del(context.Background(), id1, id2)
}
在 WebSocket 連線建立時,首先做初始化的動作,接著透過上面寫的 GetWaitFirstKey
方法查詢是否有等待聊天的 key
,如果有就進行配對並且發送訊息通知,反之將自己加入等待列表。
m.HandleConnect(func(session *melody.Session) {
id := InitSession(session)
if key, err := GetWaitFirstKey(); err == nil && key != "" {
CreateChat(id, key)
msg := NewMessage("other", "對方已經", "加入聊天室").GetByteMessage()
m.BroadcastFilter(msg, func(session *melody.Session) bool {
compareID, _ := session.Get(KEY)
return compareID == id || compareID == key
})
} else {
AddToWaitList(id)
}
})
雙方都加入聊天室後,透過剛才綁定後的結果查詢要發送訊息給誰,之後透過 BroadcastFilter
過濾接收對象。
m.HandleMessage(func(s *melody.Session, msg []byte) {
id := GetSessionID(s)
chatTo, _ := redisClient.Get(context.TODO(), id).Result()
m.BroadcastFilter(msg, func(session *melody.Session) bool {
compareID, _ := session.Get(KEY)
return compareID == chatTo || compareID == id
})
})
當其中一方關閉連線時代表聊天室已中斷,此時透過上面寫的 RemoveChat
方法將雙方的關係進行解除並通知對方。
m.HandleClose(func(session *melody.Session, i int, s string) error {
id := GetSessionID(session)
chatTo, _ := redisClient.Get(context.TODO(), id).Result()
msg := NewMessage("other", "對方已經", "離開聊天室").GetByteMessage()
RemoveChat(id, chatTo)
return m.BroadcastFilter(msg, func(session *melody.Session) bool {
compareID, _ := session.Get(KEY)
return compareID == chatTo
})
})
修改後的程式如下
package main
import (
"context"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"gopkg.in/olahol/melody.v1"
"log"
"net/http"
)
type Message struct {
Event string `json:"event"`
Name string `json:"name"`
Content string `json:"content"`
}
const (
KEY = "chat_id"
WAIT = "wait"
)
func NewMessage(event, name, content string) *Message {
return &Message{
Event: event,
Name: name,
Content: content,
}
}
func (m *Message) GetByteMessage() []byte {
result, _ := json.Marshal(m)
return result
}
var redisClient *redis.Client
func init() {
redisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "a12345", // no password set
DB: 0, // use default DB
})
pong, err := redisClient.Ping(context.Background()).Result()
if err == nil {
log.Println("redis 回應成功,", pong)
} else {
log.Fatal("redis 無法連線,錯誤為", err)
}
}
func main() {
r := gin.Default()
r.LoadHTMLGlob("template/html/*")
r.Static("/assets", "./template/assets")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
m := melody.New()
r.GET("/ws", func(c *gin.Context) {
m.HandleRequest(c.Writer, c.Request)
})
m.HandleMessage(func(s *melody.Session, msg []byte) {
id := GetSessionID(s)
chatTo, _ := redisClient.Get(context.TODO(), id).Result()
m.BroadcastFilter(msg, func(session *melody.Session) bool {
compareID, _ := session.Get(KEY)
return compareID == chatTo || compareID == id
})
})
m.HandleConnect(func(session *melody.Session) {
id := InitSession(session)
if key, err := GetWaitFirstKey(); err == nil && key != "" {
CreateChat(id, key)
msg := NewMessage("other", "對方已經", "加入聊天室").GetByteMessage()
m.BroadcastFilter(msg, func(session *melody.Session) bool {
compareID, _ := session.Get(KEY)
return compareID == id || compareID == key
})
} else {
AddToWaitList(id)
}
})
m.HandleClose(func(session *melody.Session, i int, s string) error {
id := GetSessionID(session)
chatTo, _ := redisClient.Get(context.TODO(), id).Result()
msg := NewMessage("other", "對方已經", "離開聊天室").GetByteMessage()
RemoveChat(id, chatTo)
return m.BroadcastFilter(msg, func(session *melody.Session) bool {
compareID, _ := session.Get(KEY)
return compareID == chatTo
})
})
r.Run(":5000")
}
func AddToWaitList(id string) error {
return redisClient.LPush(context.Background(), WAIT, id).Err()
}
func GetWaitFirstKey() (string, error) {
return redisClient.LPop(context.Background(), WAIT).Result()
}
func CreateChat(id1, id2 string) {
redisClient.Set(context.Background(), id1, id2, 0)
redisClient.Set(context.Background(), id2, id1, 0)
}
func RemoveChat(id1, id2 string) {
redisClient.Del(context.Background(), id1, id2)
}
func GetSessionID(s *melody.Session) string {
if id, isExist := s.Get(KEY); isExist {
return id.(string)
}
return InitSession(s)
}
func InitSession(s *melody.Session) string {
id := uuid.New().String()
s.Set(KEY, id)
return id
}
首先開啟三個分頁並且輸入 http://127.0.0.1:5000
後,可以看到第一與第二分頁呈現這樣的狀態。
接著可以在第一與第二分頁分別輸入訊息測試,兩邊可以互相接收訊息,而第三個分頁完全沒有動靜。
開啟第四個分頁輸入 http://127.0.0.1:5000
,可以看到第三個分頁也會顯示 對方已經 加入聊天室
這次結合 Redis 所製作的隨機一對一匿名聊天室範例非常的簡單,但如果流量大一點必須要結合所謂的 lock
機制才可以避免 race condition
的情況發生,後續有機會再跟各位分享!